import torch
from matplotlib import pyplot as plt
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim

# prepare dataset

batch_size = 64
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])

train_dataset = datasets.MNIST(root='../dataset/mnist/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size)
test_dataset = datasets.MNIST(root='../dataset/mnist/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_dataset, shuffle=False, batch_size=batch_size)


# design model using class


class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        #第一个卷积层 1个通道变成10个通道 卷积核为5*5 有10个通道输出则需要10个不同的卷积核参数要训练
        # Conv1d 是一维卷积层改变一个维度 Conv2d 是二维卷积层用于处理图像改变两个维度
        self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
        #第二个卷积层 10个通道变成20个通道 卷积核为5*5
        self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5)
        #最大池化层 创建一个2*2最大池化层
        self.pooling = torch.nn.MaxPool2d(2)


        #全连接层 320到10
        self.fc = torch.nn.Linear(320, 10)

    def forward(self, x):
        # flatten data from (n,1,28,28) to (n, 784) (batch,channel,w,d)
        #x[64,1,28,28]
        #size(0)即拿第一个参数即样本个数
        batch_size = x.size(0)
        #卷积 -> 池化 -> relu
        #x通过conv1 从[64,1,28,28]到[64,10,12,12]
        x = F.relu(self.pooling(self.conv1(x)))
        # x通过conv2 从[64,10,12,12]到[64,20,4,4]
        x = F.relu(self.pooling(self.conv2(x)))
        #把最后结果1变成二维  -1 此处自动算出的是320 例(batch,20,4,4)->(batch,320)
        x = x.view(batch_size, -1)
        #全连接层变换 最后一层要计算交叉熵损失 不做激活
        x = self.fc(x)
        return x


model = Net()
#使用显卡加速 cuda:0 即第一块显卡
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
#将模型转移到gpu即显卡计算
model.to(device)

# construct loss and optimizer
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)


# training cycle forward, backward, update


def train(epoch):
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        inputs, target = data
        #将数据迁移到显卡中
        inputs, target = inputs.to(device), target.to(device)
        optimizer.zero_grad()

        outputs = model(inputs)
        loss = criterion(outputs, target)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        if batch_idx % 300 == 299:
            print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))
            running_loss = 0.0


def test():
    correct = 0
    total = 0
    with torch.no_grad():
        for data in test_loader:
            images, labels = data
            # 将数据迁移到显卡中
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = torch.max(outputs.data, dim=1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print('accuracy on test set: %d %% ' % (100 * correct / total))
    return correct / total


if __name__ == '__main__':
    epoch_list = []
    acc_list = []

    for epoch in range(10):
        train(epoch)
        acc = test()
        epoch_list.append(epoch)
        acc_list.append(acc)

    plt.plot(epoch_list, acc_list)
    plt.ylabel('accuracy')
    plt.xlabel('epoch')
    plt.show()
